#ifndef SQLITE_CXX_H
#define SQLITE_CXX_H

#include <sqlite3.h>

#include <boost/intrusive_ptr.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/cstdint.hpp>

#include <string>
#include <stdexcept>


namespace sqlite_cxx
{
    class IDBConnection;
    class ISQLStatement;

    typedef boost::intrusive_ptr< IDBConnection > DBConnection;
    typedef boost::shared_ptr< ISQLStatement >    SQLStatement;

    using boost::int64_t;
    
    
    struct SQLiteError : std::runtime_error
    {
      public:
      SQLiteError( const std::string & message ) : std::runtime_error(message)
        {}
    };

    struct SQLiteBusy : SQLiteError
    {
      public:
      SQLiteBusy() : SQLiteError("Database lock acquisition timed out")
        {}
    };
    

    void throw_err( sqlite3 *conn )
    {
        throw SQLiteError( sqlite3_errmsg(conn) );
    }


    class IDBConnection
    {
        int       refcount;
        sqlite3 * conn;
   
        IDBConnection( const std::string & sqlite_db_filename,
                       bool                read_only    = false,
                       bool                allow_create = false,
                       bool                use_mutex    = false)
        {
            refcount  = 0;
            int flags = 0;
            
            flags |= read_only    ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE;
            flags |= allow_create ? SQLITE_OPEN_CREATE : 0;
            flags |= use_mutex    ? SQLITE_OPEN_FULLMUTEX : SQLITE_OPEN_NOMUTEX;
            
            if( sqlite3_open_v2( sqlite_db_filename.c_str(), &conn, flags, NULL ) != SQLITE_OK )
                throw_err(conn);
        }

      public:

        void set_busy_timeout( int ms )
        {
            sqlite3_busy_timeout( conn, ms );
        }
    
        ~IDBConnection() { sqlite3_close(conn); }
    
        operator sqlite3 * () { return conn; }

        SQLStatement sql( const char * sql_str );

        friend DBConnection open_database( const std::string &, bool, bool, bool );
        friend void intrusive_ptr_add_ref( IDBConnection * );
        friend void intrusive_ptr_release( IDBConnection * );
    };

    
    inline void intrusive_ptr_add_ref( IDBConnection * db )
    {
        ++db->refcount;
    }
    
    inline void intrusive_ptr_release( IDBConnection * db )
    {
        if( --db->refcount == 0 )
            delete db;
    }


    DBConnection open_database( const std::string & sqlite_db_filename,
                                bool                read_only    = false,
                                bool                allow_create = false,
                                bool                use_mutex    = false)
    {
        return DBConnection( new IDBConnection(sqlite_db_filename,
                                               read_only,
                                               allow_create,
                                               use_mutex) );
    }

    class ISQLStatement
    {
        DBConnection  conn;
        sqlite3_stmt *stmt;
        bool          done_flag;
        bool          data_flag;

        friend class IDBConnection;
      
      
        ISQLStatement( IDBConnection * pconn, const char * sql_str ) :
            conn(pconn), done_flag(false), data_flag(false)
        {
            if( sqlite3_prepare_v2( *conn, sql_str, -1, &stmt, NULL ) != SQLITE_OK )
                throw_err(*conn);
        }

      public:

        ~ISQLStatement()
        {
            sqlite3_finalize(stmt);
        }

        operator sqlite3_stmt * () { return stmt; }

        void reset() {
            sqlite3_reset(stmt);
            done_flag = false;
            data_flag = false;
        }
        void clear_bindings() { sqlite3_clear_bindings(stmt); }

        bool is_done() { return done_flag; }
        bool have_data() { return data_flag; }


        int column_count()
        {
            return sqlite3_column_count(stmt);
        }

        void bind( int idx, const unsigned char * blob, int nbytes, bool transient = false )
        {
            if (sqlite3_bind_blob(stmt, idx, blob, nbytes,
                                  transient ? SQLITE_TRANSIENT : SQLITE_STATIC) != SQLITE_OK)
                throw_err(*conn);
        }

        void bind( int idx, const char * c_str, int nbytes = -1, bool transient = false )
        {
            if (nbytes < 0)
                nbytes = strlen( c_str );
            
            if (sqlite3_bind_blob(stmt, idx, c_str, nbytes,
                                  transient ? SQLITE_TRANSIENT : SQLITE_STATIC) != SQLITE_OK)
                throw_err(*conn);
        }

        void bind( int idx, double v )
        {
            if (sqlite3_bind_double(stmt, idx, v) != SQLITE_OK)
                throw_err(*conn);
        }

        void bind( int idx, int v )
        {
            if (sqlite3_bind_int(stmt, idx, v) != SQLITE_OK)
                throw_err(*conn);
        }

        void bind( int idx, int64_t v )
        {
            if (sqlite3_bind_int64(stmt, idx, v) != SQLITE_OK)
                throw_err(*conn);
        }

        void bind_null( int idx )
        {
            if (sqlite3_bind_null(stmt, idx) != SQLITE_OK)
                throw_err(*conn);
        }

        template <class T>
        void bind( const char * param_name, T v )
        {
            bind( sqlite3_bind_parameter_index(stmt, param_name), v );
        }

        bool step( bool throw_err_on_busy = true )
        {
            data_flag = false;
            
            int ret;
            
            do
            {
                ret = sqlite3_step(stmt);
                
                if (throw_err_on_busy && ret == SQLITE_BUSY)
                    throw SQLiteBusy();
                
            } while( ret == SQLITE_BUSY );


            if ( ret == SQLITE_DONE )
                done_flag = true;

            if ( ret == SQLITE_ROW )
                data_flag = true;

            return data_flag;
        }

        int column_type( int col )
        {
            return sqlite3_column_type(stmt, col);
        }

        const void * column_blob( int col )
        {
            return sqlite3_column_blob(stmt, col);
        }

        int column_bytes( int col )
        {
            return sqlite3_column_bytes(stmt, col);
        }

        double column_double( int col )
        {
            return sqlite3_column_double(stmt, col);
        }

        int column_int( int col )
        {
            return sqlite3_column_int(stmt, col);
        }

        int64_t column_int64( int col )
        {
            return sqlite3_column_int64(stmt, col);
        }

        const unsigned char * column_text( int col )
        {
            return sqlite3_column_text(stmt, col);
        }
    };



    SQLStatement IDBConnection::sql( const char * sql_str )
    {
        return SQLStatement( new ISQLStatement( this, sql_str ) );
    }
    
}// end namespace sqlite_cxx

#endif

